void AsyncWrap::DestroyAsyncIdsCallback(Environment* env) {
Local<Function> fn = env->async_hooks_destroy_function();
- TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal);
+ TryCatchScope try_catch(env,
+ TryCatchScope::CatchMode::kFatalRethrowStackOverflow);
do {
std::vector<double> destroy_async_id_list;
HandleScope handle_scope(env->isolate());
Local<Value> async_id_value = Number::New(env->isolate(), async_id);
- TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal);
+ TryCatchScope try_catch(env,
+ TryCatchScope::CatchMode::kFatalRethrowStackOverflow);
USE(fn->Call(env->context(), Undefined(env->isolate()), 1, &async_id_value));
}
object,
};
- TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal);
+ TryCatchScope try_catch(env,
+ TryCatchScope::CatchMode::kFatalRethrowStackOverflow);
USE(init_fn->Call(env->context(), object, arraysize(argv), argv));
}
}
Local<StackTrace> stack;
- if (!GetCurrentStackTrace(isolate).ToLocal(&stack)) {
+ if (!GetCurrentStackTrace(isolate).ToLocal(&stack) ||
+ stack->GetFrameCount() == 0) {
return;
}
}
static std::atomic<bool> is_in_oom{false};
-static std::atomic<bool> is_retrieving_js_stacktrace{false};
+static thread_local std::atomic<bool> is_retrieving_js_stacktrace{false};
MaybeLocal<StackTrace> GetCurrentStackTrace(Isolate* isolate, int frame_count) {
if (isolate == nullptr) {
return MaybeLocal<StackTrace>();
StackTrace::CurrentStackTrace(isolate, frame_count, options);
is_retrieving_js_stacktrace.store(false);
- if (stack->GetFrameCount() == 0) {
- return MaybeLocal<StackTrace>();
- }
return scope.Escape(stack);
}
void PrintCurrentStackTrace(Isolate* isolate, StackTracePrefix prefix) {
Local<StackTrace> stack;
- if (GetCurrentStackTrace(isolate).ToLocal(&stack)) {
+ if (GetCurrentStackTrace(isolate).ToLocal(&stack) &&
+ stack->GetFrameCount() > 0) {
PrintStackTrace(isolate, stack, prefix);
}
}
};
}
+// Check if an exception is a stack overflow error (RangeError with
+// "Maximum call stack size exceeded" message). This is used to handle
+// stack overflow specially in TryCatchScope - instead of immediately
+// exiting, we can use the red zone to re-throw to user code.
+static bool IsStackOverflowError(Isolate* isolate, Local<Value> exception) {
+ if (!exception->IsNativeError()) return false;
+
+ Local<Object> err_obj = exception.As<Object>();
+ Local<String> constructor_name = err_obj->GetConstructorName();
+
+ // Must be a RangeError
+ Utf8Value name(isolate, constructor_name);
+ if (name.ToStringView() != "RangeError") return false;
+
+ // Check for the specific stack overflow message
+ Local<Context> context = isolate->GetCurrentContext();
+ Local<Value> message_val;
+ if (!err_obj->Get(context, String::NewFromUtf8Literal(isolate, "message"))
+ .ToLocal(&message_val)) {
+ return false;
+ }
+
+ if (!message_val->IsString()) return false;
+
+ Utf8Value message(isolate, message_val.As<String>());
+ return message.ToStringView() == "Maximum call stack size exceeded";
+}
+
namespace errors {
TryCatchScope::~TryCatchScope() {
- if (HasCaught() && !HasTerminated() && mode_ == CatchMode::kFatal) {
+ if (HasCaught() && !HasTerminated() && mode_ != CatchMode::kNormal) {
HandleScope scope(env_->isolate());
Local<v8::Value> exception = Exception();
Local<v8::Message> message = Message();
+
+ // Special handling for stack overflow errors in async_hooks: instead of
+ // immediately exiting, re-throw the exception. This allows the exception
+ // to propagate to user code's try-catch blocks.
+ if (mode_ == CatchMode::kFatalRethrowStackOverflow &&
+ IsStackOverflowError(env_->isolate(), exception)) {
+ ReThrow();
+ Reset();
+ return;
+ }
+
EnhanceFatalException enhance = CanContinue() ?
EnhanceFatalException::kEnhance : EnhanceFatalException::kDontEnhance;
if (message.IsEmpty())
if (env->can_call_into_js()) {
// We do not expect the global uncaught exception itself to throw any more
// exceptions. If it does, exit the current Node.js instance.
- errors::TryCatchScope try_catch(env,
- errors::TryCatchScope::CatchMode::kFatal);
+ // Special case: if the original error was a stack overflow and calling
+ // _fatalException causes another stack overflow, rethrow it to allow
+ // user code's try-catch blocks to potentially catch it.
+ auto is_stack_overflow = [&] {
+ return IsStackOverflowError(env->isolate(), error);
+ };
+ // Without a JS stack, rethrowing may or may not do anything.
+ // TODO(addaleax): In V8, expose a way to check whether there is a JS stack
+ // or TryCatch that would capture the rethrown exception.
+ auto has_js_stack = [&] {
+ HandleScope handle_scope(env->isolate());
+ Local<StackTrace> stack;
+ return GetCurrentStackTrace(env->isolate(), 1).ToLocal(&stack) &&
+ stack->GetFrameCount() > 0;
+ };
+ errors::TryCatchScope::CatchMode mode =
+ is_stack_overflow() && has_js_stack()
+ ? errors::TryCatchScope::CatchMode::kFatalRethrowStackOverflow
+ : errors::TryCatchScope::CatchMode::kFatal;
+ errors::TryCatchScope try_catch(env, mode);
// Explicitly disable verbose exception reporting -
// if process._fatalException() throws an error, we don't want it to
// trigger the per-isolate message listener which will call this
class TryCatchScope : public v8::TryCatch {
public:
- enum class CatchMode { kNormal, kFatal };
+ enum class CatchMode { kNormal, kFatal, kFatalRethrowStackOverflow };
explicit TryCatchScope(Environment* env, CatchMode mode = CatchMode::kNormal)
: v8::TryCatch(env->isolate()), env_(env), mode_(mode) {}
const char* trigger) {
HandleScope scope(isolate);
Local<v8::StackTrace> stack;
- if (!GetCurrentStackTrace(isolate, MAX_FRAME_COUNT).ToLocal(&stack)) {
+ if (!GetCurrentStackTrace(isolate, MAX_FRAME_COUNT).ToLocal(&stack) ||
+ stack->GetFrameCount() == 0) {
PrintEmptyJavaScriptStack(writer);
return;
}
--- /dev/null
+'use strict';
+
+// This test verifies that stack overflow during deeply nested async operations
+// with async_hooks enabled can be caught by try-catch. This simulates real-world
+// scenarios like processing deeply nested JSON structures where each level
+// creates async operations (e.g., database calls, API requests).
+
+require('../common');
+const assert = require('assert');
+const { spawnSync } = require('child_process');
+
+if (process.argv[2] === 'child') {
+ const { createHook } = require('async_hooks');
+
+ // Enable async_hooks with all callbacks (simulates APM tools)
+ createHook({
+ init() {},
+ before() {},
+ after() {},
+ destroy() {},
+ promiseResolve() {},
+ }).enable();
+
+ // Simulate an async operation (like a database call or API request)
+ async function fetchThing(id) {
+ return { id, data: `data-${id}` };
+ }
+
+ // Recursively process deeply nested data structure
+ // This will cause stack overflow when the nesting is deep enough
+ function processData(data, depth = 0) {
+ if (Array.isArray(data)) {
+ for (const item of data) {
+ // Create a promise to trigger async_hooks init callback
+ fetchThing(depth);
+ processData(item, depth + 1);
+ }
+ }
+ }
+
+ // Create deeply nested array structure iteratively (to avoid stack overflow
+ // during creation)
+ function createNestedArray(depth) {
+ let result = 'leaf';
+ for (let i = 0; i < depth; i++) {
+ result = [result];
+ }
+ return result;
+ }
+
+ // Create a very deep nesting that will cause stack overflow during processing
+ const deeplyNested = createNestedArray(50000);
+
+ try {
+ processData(deeplyNested);
+ // Should not complete successfully - the nesting is too deep
+ console.log('UNEXPECTED: Processing completed without error');
+ process.exit(1);
+ } catch (err) {
+ assert.strictEqual(err.name, 'RangeError');
+ assert.match(err.message, /Maximum call stack size exceeded/);
+ console.log('SUCCESS: try-catch caught the stack overflow in nested async');
+ process.exit(0);
+ }
+} else {
+ // Parent process - spawn the child and check exit code
+ const result = spawnSync(
+ process.execPath,
+ [__filename, 'child'],
+ { encoding: 'utf8', timeout: 30000 }
+ );
+
+ // Should exit successfully (try-catch worked)
+ assert.strictEqual(result.status, 0,
+ `Expected exit code 0, got ${result.status}.\n` +
+ `stdout: ${result.stdout}\n` +
+ `stderr: ${result.stderr}`);
+ // Verify the error was handled by try-catch
+ assert.match(result.stdout, /SUCCESS: try-catch caught the stack overflow/);
+}
--- /dev/null
+'use strict';
+
+// This test verifies that when a stack overflow occurs with async_hooks
+// enabled, the exception can be caught by try-catch blocks in user code.
+
+require('../common');
+const assert = require('assert');
+const { spawnSync } = require('child_process');
+
+if (process.argv[2] === 'child') {
+ const { createHook } = require('async_hooks');
+
+ createHook({ init() {} }).enable();
+
+ function recursive(depth = 0) {
+ // Create a promise to trigger async_hooks init callback
+ new Promise(() => {});
+ return recursive(depth + 1);
+ }
+
+ try {
+ recursive();
+ // Should not reach here
+ process.exit(1);
+ } catch (err) {
+ assert.strictEqual(err.name, 'RangeError');
+ assert.match(err.message, /Maximum call stack size exceeded/);
+ console.log('SUCCESS: try-catch caught the stack overflow');
+ process.exit(0);
+ }
+
+ // Should not reach here
+ process.exit(2);
+} else {
+ // Parent process - spawn the child and check exit code
+ const result = spawnSync(
+ process.execPath,
+ [__filename, 'child'],
+ { encoding: 'utf8', timeout: 30000 }
+ );
+
+ assert.strictEqual(result.status, 0,
+ `Expected exit code 0 (try-catch worked), got ${result.status}.\n` +
+ `stdout: ${result.stdout}\n` +
+ `stderr: ${result.stderr}`);
+ assert.match(result.stdout, /SUCCESS: try-catch caught the stack overflow/);
+}
--- /dev/null
+'use strict';
+
+// This test verifies that when a stack overflow occurs with async_hooks
+// enabled, the uncaughtException handler is still called instead of the
+// process crashing with exit code 7.
+
+const common = require('../common');
+const assert = require('assert');
+const { spawnSync } = require('child_process');
+
+if (process.argv[2] === 'child') {
+ const { createHook } = require('async_hooks');
+
+ let handlerCalled = false;
+
+ function recursive() {
+ // Create a promise to trigger async_hooks init callback
+ new Promise(() => {});
+ return recursive();
+ }
+
+ createHook({ init() {} }).enable();
+
+ process.on('uncaughtException', common.mustCall((err) => {
+ assert.strictEqual(err.name, 'RangeError');
+ assert.match(err.message, /Maximum call stack size exceeded/);
+ // Ensure handler is only called once
+ assert.strictEqual(handlerCalled, false);
+ handlerCalled = true;
+ }));
+
+ setImmediate(recursive);
+} else {
+ // Parent process - spawn the child and check exit code
+ const result = spawnSync(
+ process.execPath,
+ [__filename, 'child'],
+ { encoding: 'utf8', timeout: 30000 }
+ );
+
+ // Should exit with code 0 (handler was called and handled the exception)
+ // Previously would exit with code 7 (kExceptionInFatalExceptionHandler)
+ assert.strictEqual(result.status, 0,
+ `Expected exit code 0, got ${result.status}.\n` +
+ `stdout: ${result.stdout}\n` +
+ `stderr: ${result.stderr}`);
+}
--- /dev/null
+'use strict';
+
+// This test verifies that when the uncaughtException handler itself causes
+// a stack overflow, the process exits with a non-zero exit code.
+// This is important to ensure we don't silently swallow errors.
+
+require('../common');
+const assert = require('assert');
+const { spawnSync } = require('child_process');
+
+if (process.argv[2] === 'child') {
+ function f() { f(); }
+ process.on('uncaughtException', f);
+ f();
+} else {
+ // Parent process - spawn the child and check exit code
+ const result = spawnSync(
+ process.execPath,
+ [__filename, 'child'],
+ { encoding: 'utf8', timeout: 30000 }
+ );
+
+ // Should exit with non-zero exit code since the uncaughtException handler
+ // itself caused a stack overflow.
+ assert.notStrictEqual(result.status, 0,
+ `Expected non-zero exit code, got ${result.status}.\n` +
+ `stdout: ${result.stdout}\n` +
+ `stderr: ${result.stderr}`);
+}
--- /dev/null
+'use strict';
+
+// This test verifies that when the uncaughtException handler itself causes
+// a stack overflow, the process exits with a non-zero exit code.
+// This is important to ensure we don't silently swallow errors.
+
+require('../common');
+const assert = require('assert');
+const { spawnSync } = require('child_process');
+
+if (process.argv[2] === 'child') {
+ function f() { f(); }
+ process.on('uncaughtException', f);
+ throw new Error('X');
+} else {
+ // Parent process - spawn the child and check exit code
+ const result = spawnSync(
+ process.execPath,
+ [__filename, 'child'],
+ { encoding: 'utf8', timeout: 30000 }
+ );
+
+ // Should exit with non-zero exit code since the uncaughtException handler
+ // itself caused a stack overflow.
+ assert.notStrictEqual(result.status, 0,
+ `Expected non-zero exit code, got ${result.status}.\n` +
+ `stdout: ${result.stdout}\n` +
+ `stderr: ${result.stderr}`);
+}